/*
 * Implementacion para la Generacion de Codigo para el lenguaje C Minus.
 * 
 * Basado en el proyecto descrito en: 
 * Louden, K. (1997). Compiler Construction: Principles and Practice. Boston: PWS.
 * 
 * @ Project: Compiler
 * @ File Name: CodeGenerator.java
 */
package cml.coding;

import java.io.FileNotFoundException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Hashtable;

import cml.Lexicon;
import cml.TokenType;
import cml.Type;
import cml.analyzing.*;
import cml.parsing.tree.*;

public class CodeGenerator {
	
	private CodeWriter cw;
	private StringBuffer codeVars;
	private StringBuffer codeFunc;
	private StringBuffer codeLabels;
	private StringBuffer codeBody;
	private Hashtable<String, Integer> funcLoc;
	private Hashtable<String, String> calls; 
	private int labelCount;
	private int memLoc;
	private ArrayList<SymTable> tables;
	private SymTable currentTable;
	private String currentScope;
	private boolean trace;
	private String code;
	
	/*
	 * Constructor de la clase
	 */
	public CodeGenerator(String code, ArrayList<SymTable> tables) {
		
		this.code = code;
		
		try {
			this.cw = new CodeWriter(code);
		} 
		catch (FileNotFoundException e) {
			
			e.printStackTrace();
		} 
		catch (UnsupportedEncodingException e) {
			
			e.printStackTrace();
		}
		
		this.tables = tables;
		this.codeVars = new StringBuffer();
		this.codeFunc = new StringBuffer();
		this.codeLabels = new StringBuffer();
		this.codeBody = new StringBuffer();
		this.funcLoc = new Hashtable<String, Integer>();
		this.calls = new Hashtable<String, String>();
		this.labelCount = 0;
		this.memLoc = 0;
		this.trace = false;
	}
	
	/*
	 * Genera el codigo equivalente al nodo sintactico.
	 * 
	 * @param	node	nodo sintactico
	 */
	private void codeGen(SynNode node) {
		
		while(node != null) {
			
			if(node instanceof DeclNode)
				this.declGen(node);
			else if(node instanceof StmtNode)
				this.stmtGen((StmtNode)node);
			else if(node instanceof ExpNode)
				this.expGen((ExpNode)node);
			
			node = node.getSibling();
		}
	}
	
	/*
	 * Genera el codigo para declaraciones.
	 * 
	 * @param	node sintactico de declaracion
	 */
	private void declGen(SynNode node) {
		
		if(node != null) {
			
			if(node instanceof FuncDeclNode) {
				
				FuncDeclNode temp = (FuncDeclNode)node;
				String tableName = temp.getName() + FuncSymbol.getFuncSignatureString(
																FuncSymbol.getParamsType(temp));
				
				this.funcLoc.put(temp.getName(), Emit.emitLoc());
				
				SymTable previousTable = this.currentTable;
				SymTable newTable = Tables.lookupSymTable(this.tables.get(Analyzer.GLOBAL_TABLE).getName() + "." + 
															tableName, this.tables);
				
				if(newTable != null) {
					
					this.currentScope = temp.getName();
					newTable.setActive(true);
					this.currentTable = newTable;
				}
				
				if(trace)
					this.codeBody.append(Emit.comment("Inicio de Funcion '" + temp.getName() + "'") + "\n");
				
				this.codeGen(temp.getChild(FuncDeclNode.PARAMS));
				this.codeGen(temp.getChild(FuncDeclNode.BODY));
				this.codeBody.append(Emit.opr(Emit.RET_OPR));
				
				if(trace)
					this.codeBody.append("\t" + Emit.comment("Regreso de funcion") + "\n");
				else this.codeBody.append("\n");
				
				if(trace)
					this.codeBody.append(Emit.comment("Fin de Funcion '" + temp.getName() + "'") + "\n");
				
				if(newTable != null) {
					
					newTable.setActive(false);
					this.currentTable = previousTable;
				}
			}
			else if(node instanceof ParamNode) {
				
				ParamNode temp = (ParamNode)node;
				
				this.codeBody.append(Emit.sto(temp.getName()));
				
				if(trace)
					this.codeBody.append("\t" + Emit.comment("Parametro '" + temp.getName() + "'") + "\n");
				else this.codeBody.append("\n");
			}
			else if(node instanceof VarDeclNode) {
				
				VarDeclNode temp = (VarDeclNode)node;
				
				Symbol vs = Tables.lookupVarSymbol(temp.getName(), true, this.tables);
				
				if(vs != null)
					this.addVar(vs.getName(), vs.getType(), this.currentScope);
			}
		}
	}
	
	/*
	 * Genera el codigo para enunciados.
	 * 
	 * @param	node sintactico de enunciado
	 */
	private void stmtGen(StmtNode node) {
		
		String label1 = ""; 
		String label2 = "";
		int locL1 = -1; 
		int locL2 = -1;
		
		if(node != null) {
			
			if(node instanceof IfNode) {
				
				SymTable previousTable = this.currentTable;
				SymTable newTable = Tables.lookupSymTable(this.currentTable.getName() + ".if", this.tables);
				
				if(newTable != null) {
					
					newTable.setActive(true);
					this.currentTable = newTable;
				}
				
				if(trace)
					this.codeBody.append(Emit.comment("Inicio de If") + "\n");
				
				IfNode temp = (IfNode)node;
				
				if(trace)
					this.codeBody.append(Emit.comment("Prueba de If") + "\n");
				this.codeGen(temp.getChild(IfNode.TEST));
				
				label1 = this.nextLabel();
				this.codeBody.append(Emit.jmc(false, label1) + "\n");
				
				if(trace)
					this.codeBody.append(Emit.comment("Cuerpo de If") + "\n");
				this.codeGen(temp.getChild(IfNode.BODY));
				
				if(temp.getChild(IfNode.ELSE) != null) {
					
					label2 = this.nextLabel();
					this.codeBody.append(Emit.jmp(label2) + "\n");
				}
				
				locL1 = Emit.emitLoc();
				this.addLabel(label1, locL1);
				
				if(temp.getChild(IfNode.ELSE) != null) {
					
					this.codeGen(temp.getChild(IfNode.ELSE));
					
					locL2 = Emit.emitLoc();
					this.addLabel(label2, locL2);
				}
				
				if(trace)
					this.codeBody.append(Emit.comment("Fin de If") + "\n");
				
				if(newTable != null) {
					
					newTable.setActive(false);
					this.currentTable = previousTable;
				}
			}
			else if(node instanceof ElseNode) {
				
				SymTable previousTable = this.currentTable;
				SymTable newTable = Tables.lookupSymTable(this.currentTable.getName() + ".else", this.tables);
				
				if(newTable != null) {
					
					newTable.setActive(true);
					this.currentTable = newTable;
				}
				
				ElseNode temp = (ElseNode)node;
				
				if(trace)
					this.codeBody.append(Emit.comment("Cuerpo de Else") + "\n");
				this.codeGen(temp.getChild(ElseNode.BODY));
				
				if(newTable != null) {
					
					newTable.setActive(false);
					this.currentTable = previousTable;
				}
			}
			else if(node instanceof WhileNode) {
				
				SymTable previousTable = this.currentTable;
				SymTable newTable = Tables.lookupSymTable(this.currentTable.getName() + ".while", this.tables);
				
				if(newTable != null) {
					
					newTable.setActive(true);
					this.currentTable = newTable;
				}
				
				if(trace)
					this.codeBody.append(Emit.comment("Inicio de While") + "\n");
				
				WhileNode temp = (WhileNode)node;
				
				label1 = this.nextLabel();
				locL1 = Emit.emitLoc();
				this.addLabel(label1, locL1);
				
				if(trace)
					this.codeBody.append(Emit.comment("Prueba de While") + "\n");
				this.codeGen(temp.getChild(WhileNode.TEST));
				
				label2 = this.nextLabel();
				this.codeBody.append(Emit.jmc(false, label2) + "\n");
				
				if(trace)
					this.codeBody.append(Emit.comment("Cuerpo de While") + "\n");
				this.codeGen(temp.getChild(WhileNode.BODY));
				
				this.codeBody.append(Emit.jmp(label1) + "\n");
				
				locL2 = Emit.emitLoc();
				this.addLabel(label2, locL2);
				
				if(trace)
					this.codeBody.append(Emit.comment("Fin de While") + "\n");
				
				if(newTable != null) {
					
					newTable.setActive(false);
					this.currentTable = previousTable;
				}
			}
			else if(node instanceof ReturnNode) {
				
				ReturnNode temp = (ReturnNode)node;
				
				this.codeGen(temp.getChild(ReturnNode.BODY));
			}
		}
	}
	
	/*
	 * Genera el codigo para expresiones.
	 * 
	 * @param	node sintactico de expresion
	 */
	private void expGen(ExpNode node) {
		
		if(node != null) {
			
			if(node instanceof NumNode) {
				
				NumNode temp = (NumNode)node;
				Object value = temp.getValue();
				
				if(value instanceof Integer) {
					
					Integer v = (Integer)value;
					
					this.codeBody.append(Emit.lit(v.intValue()));
					
					if(trace)
						this.codeBody.append("\t" + Emit.comment("Entero " + v) + "\n");
					else this.codeBody.append("\n");
				}
				else if(value instanceof Double) {
					
					Double v = (Double)value;
					
					this.codeBody.append(Emit.lit(v.doubleValue()));
					
					if(trace)
						this.codeBody.append("\t" + Emit.comment("Decimal " + v) + "\n");
					else this.codeBody.append("\n");
				}
			}
			else if(node instanceof BoolNode) {
				
				BoolNode temp = (BoolNode)node;
				
				this.codeBody.append(Emit.lit(temp.getValue()));
				
				if(trace)
					this.codeBody.append("\t" + Emit.comment("Booleano") + "\n");
				else this.codeBody.append("\n");
			}
			else if(node instanceof TextNode) {
				
				TextNode temp = (TextNode)node;
				
				this.codeBody.append(Emit.lit(temp.getValue()));
				
				if(trace)
					this.codeBody.append("\t" + Emit.comment("Texto") + "\n");
				else this.codeBody.append("\n");
			}
			else if(node instanceof VarNode) {
				
				VarNode temp = (VarNode)node;
				
				this.codeBody.append(Emit.lod(temp.getName()));
				
				if(trace)
					this.codeBody.append("\t" + Emit.comment("Var '" + temp.getName() + "'") + "\n");
				else this.codeBody.append("\n");
			}
			else if(node instanceof OpNode) {
				
				OpNode temp = (OpNode)node;
				String name = temp.getName();
				
				if(!name.equals(TokenType.ASSIGN.toString())) {
					
					this.codeGen(temp.getChild(OpNode.LEFT));
					this.codeGen(temp.getChild(OpNode.RIGHT));
					this.codeBody.append(Emit.opr(this.opToOprCode(temp.getName())));
					
					if(trace)
						this.codeBody.append("\t" + Emit.comment("Operacion '" + temp.getName() + "'") + "\n");
					else this.codeBody.append("\n");
				}
				else {
					
					this.codeGen(temp.getChild(OpNode.RIGHT));
					
					VarNode var = (VarNode)temp.getChild(OpNode.LEFT);
					
					this.codeBody.append(Emit.sto(var.getName()));
					
					if(trace)
						this.codeBody.append("\t" + Emit.comment("Asignacion de '" + var.getName() + "'") + "\n");
					else this.codeBody.append("\n");
				}
			}
			else if(node instanceof CallNode) {
				
				CallNode temp = (CallNode)node;
				String name = temp.getName();
				
				if(name.equals(Lexicon.FUNC_PRINT)) {
					
					this.codeGen(temp.getChild(CallNode.ARGS));
					this.codeBody.append(Emit.opr(Emit.WRI_OPR));
					
					if(trace)
						this.codeBody.append("\t" + Emit.comment("Imprimir en consola") + "\n");
					else this.codeBody.append("\n");
				}
				else if(name.equals(Lexicon.FUNC_READ)) {
					
					this.codeGen(temp.getChild(CallNode.ARGS));
					this.codeBody.append(Emit.opr(Emit.RDI_OPR));
					
					if(trace)
						this.codeBody.append("\t" + Emit.comment("Leer de consola") + "\n");
					else this.codeBody.append("\n");
				}
				else {
					
					String label = this.nextLabel();
					
					this.codeBody.append(Emit.lod(label) + "\n");
					this.codeGen(temp.getChild(CallNode.ARGS));
					
					String call = Emit.cal(temp.getName());
					this.codeBody.append(call);
					int i = this.codeBody.indexOf(call);
					this.calls.put(temp.getName(), i + "-" + (i + call.length()));
					
					if(trace)
						this.codeBody.append("\t" + Emit.comment("Llamada a funcion '" + temp.getName() +"'") + 
											"\n");
					else this.codeBody.append("\n");
					
					this.addLabel(label, Emit.emitLoc());
					
					this.codeBody.append(Emit.lod(temp.getName()) + "\n");
				}
			}
		}
	}
	
	/*
	 * Regresa el numero de operacion especial de Codigo P a partir de el operador.
	 * 
	 * @param	name	nombre del operador
	 * @return	codigo de la operacion de Codigo P
	 */
	private int opToOprCode(String name) {
		
		if(name.equals(TokenType.PLUS.toString()))
			return Emit.ADD_OPR;
		else if(name.equals(TokenType.MINUS.toString()))
			return Emit.SUB_OPR;
		else if(name.equals(TokenType.TIMES.toString()))
			return Emit.MUL_OPR;
		else if(name.equals(TokenType.OVER.toString()))
			return Emit.DIV_OPR;
		else if(name.equals(TokenType.GT.toString()))
			return Emit.GT_OPR;
		else if(name.equals(TokenType.GTEQ.toString()))
			return Emit.GTEQ_OPR;
		else if(name.equals(TokenType.EQ.toString()))
			return Emit.EQ_OPR;
		else if(name.equals(TokenType.NEQ.toString()))
			return Emit.NEQ_OPR;
		else if(name.equals(TokenType.LT.toString()))
			return Emit.LT_OPR;
		else return Emit.LTEQ_OPR;
	}
	
	/*
	 * Se encarga de actualizar todo aquello que quedo incompleto en el codigo.
	 * 
	 * @param	node	nodo sintactico
	 */
	private void backpatchCode(SynNode node) {
		
		while(node != null) {
			
			if(node instanceof FuncDeclNode) {
				
				FuncDeclNode temp = (FuncDeclNode)node;
				String funcName = temp.getName() + FuncSymbol.getFuncSignatureString(
																FuncSymbol.getParamsType(temp));
				int funcLoc = this.funcLoc.get(temp.getName());
				
				FuncSymbol fs = Tables.lookupFuncSymbol(funcName, this.tables);
				this.addFunc(temp.getName(), fs.getType(), funcLoc);
				
				String c = this.calls.get(temp.getName());
				
				if(c != null) {
					
					int start = Integer.parseInt(c.split("-")[0]);
					int end = Integer.parseInt(c.split("-")[1]);
					
					String call = this.codeBody.substring(start, end);
					call = Emit.updateCal(call, funcLoc);
					this.codeBody.replace(start, end, call);
				}
			}
			
			node = node.getSibling();
		}
	}
	
	/*
	 * Regresa la siguiente etiqueta.
	 * 
	 * @return	nombre de la etiqueta
	 */
	private String nextLabel() {
		
		return "_L" + this.labelCount++;
	}
	
	/*
	 * Agrega la declaracion de una etiqueta.
	 * 
	 * @param	name	nombre
	 * @param	loc		ubicacion de la etiqueta en el codigo
	 */
	private void addLabel(String name, int loc) {
		
		this.codeLabels.append(Emit.label(name, loc) + "\n");
	}
	
	/*
	 * Agrega la declaracion de una funcion.
	 * 
	 * @param	name	nombre de la funcion
	 * @param	type	tipo de regreso de la funcion
	 * @param	loc		ubicacion de la funcion en el codigo
	 */
	private void addFunc(String name, Type type, int loc) {
		
		String t = type.toString().substring(0, 1).toUpperCase();
		
		this.codeFunc.append(Emit.func(name, t, loc) + "\n");
	}
	
	/*
	 * Agrega la declaracion de una variable.
	 * 
	 * @param	name	nombre de la variable
	 * @param	type	tipo de dato de la variable
	 * @param	scope	bloque de la variable
	 */
	private void addVar(String name, Type type, String scope) {
		
		String t = type.toString().substring(0, 1).toUpperCase();
		
		this.codeVars.append(Emit.var(name, t, scope, this.nextMemLoc()) + "\n");
	}
	
	/*
	 * Regresa la siguiente ubicacion en memoria.
	 * 
	 * @return	ubicacion en memoria
	 */
	private int nextMemLoc() {
		
		return this.memLoc++;
	}
	
	/*
	 * Genera el equivalente a Codigo P a partir del Arbol Sintactico.
	 * 
	 * @param	tree	arbol sintactico
	 */
	public void generateCode(SynNode tree) {
		
		this.codeVars.append(Emit.comment("Compilacion C Minus a Codigo P") + "\n");
		this.codeVars.append(Emit.comment("Archivo: " + this.code) + "\n");
		this.codeVars.append(Emit.comment("Inicio Encabezado") + "\n");
		if(trace) {
			
			this.codeVars.append(Emit.comment("Variables") + "\n");
			this.codeFunc.append(Emit.comment("Funciones") + "\n");
			this.codeLabels.append(Emit.comment("Etiquetas") + "\n");
		}
		this.codeBody.append(Emit.comment("Inicio del Programa") + "\n");
		
		this.currentScope = this.tables.get(0).getName();
		
		this.codeGen(tree);
		this.backpatchCode(tree);
		
		this.codeLabels.append(Emit.comment("Fin Encabezado") + "\n");
		this.codeBody.append(Emit.opr(Emit.END_OPR));
		this.codeBody.append(Emit.comment("\tFin del Programa") + "\n");
		
		String program = this.codeVars.toString() + this.codeFunc.toString() + this.codeLabels.toString() + 
						Emit.SEPARATOR + "\n" + this.codeBody.toString();
		
		if(trace)
			System.out.print(program);
		
		this.cw.writeToFile(program);
		this.cw.closeFile();
	}
	
	/*
	 * Habilita/deshabilita el mostrar en consola un reporte de los 
	 * <code>tokens</code> encontrados.
	 * 
	 * @param	boolean	si <code>true</code>, muestra el reporte; en caso
	 * 					contrario no lo hace
	 */
	public void setTrace(boolean t) {
		
		this.trace = t;
	}
}
